/*

Copyright (c) 2014, 2015, 2016, 2017 RonxBulld

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

*/

#ifndef CXXOPTS_YACO_H
#define CXXOPTS_YACO_H

#include <functional>
#include "cxxopts.hpp"

/// Flags
namespace yaco {

enum FormattingFlags {
  NormalFormatting = 0x00,
  Positional       = 0x01,
  Prefix           = 0x02,
  AlwaysPrefix     = 0x03
};

enum class Occurrence { // FIXME: Not implement.
  Optional,
  ZeroOrMore,
  Required,
  OneOrMore,
  ConsumeAfter
};

enum class ValueAllow {
  ValueOptional,
  ValueRequired,
  ValueDisallowed
};

}

/// Exception
namespace yaco {

class option_multiset_error : public cxxopts::OptionSpecException
{
public:
  explicit option_multiset_error(const std::string &option)
      : OptionSpecException("Option " + cxxopts::LQUOTE + option +
                            cxxopts::RQUOTE + " is set multiple times.")
  {}
};

class option_multibind_error : public cxxopts::OptionSpecException
{
public:
  explicit option_multibind_error(const std::string &option)
      : OptionSpecException("Option " + cxxopts::LQUOTE + option +
                            cxxopts::RQUOTE + " is bind multiple parsers.")
  {}
};

class option_nonbind_error : public cxxopts::OptionSpecException
{
public:
  explicit option_nonbind_error(const std::string &option)
      : OptionSpecException("Option " + cxxopts::LQUOTE + option +
                            cxxopts::RQUOTE + " is not bind any parser.")
  {}
};

class category_multibind_error : public cxxopts::OptionSpecException
{
public:
  explicit category_multibind_error(const std::string &option)
      : OptionSpecException("Option " + cxxopts::LQUOTE + option +
                            cxxopts::RQUOTE + " is bind multiple categories.")
  {}
};
}

/// applicator
namespace yaco {

using cxxopts::ErrOr;

// The prototype of applicator.
// This way of using static functions in a class enables partial specialization.
template <class Action> struct applicator
{
  template <class Opt> static void apply(const Action &action, Opt &O) {
    action.Apply(O);
  }
};

//===========================================================================
// ApplyAction

template <typename Obj>
void ApplyAction(Obj &O)
{
  (void) O;
}

template <typename Obj, typename Action>
void ApplyAction(Obj &O, const Action &action)
{
  applicator<Action>::apply(action, O);
}

template <typename Obj, typename Action, typename... Actions>
void ApplyAction(Obj &O, const Action &action, const Actions &...actions)
{
  ApplyAction(O, action);
  ApplyAction(O, actions...);
}
}

/// Value storage
namespace yaco {

template <typename T, bool is_class> struct ValueStorage
{
  virtual ~ValueStorage() = default;
};

#if defined(__GNUC__)
// GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we want to silence it: warning: base class 'class std::enable_shared_from_this<cxxopts::Value>' has accessible non-virtual destructor
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnon-virtual-dtor"
#pragma GCC diagnostic ignored "-Weffc++"
// This will be ignored under other compilers like LLVM clang.
#endif
template <typename T> struct ValueStorage<T, true> : public T
{
  virtual ~ValueStorage() = default;

  void StoreValue(const T &V, bool is_default = false)
  {
    T::operator=(V);
    if (is_default) {
      default_value_ = V;
    }
  }
  T &GetValue()
  {
    return *this;
  }
  const T &GetValue() const
  {
    return *this;
  }
  operator T() const
  {
    return GetValue();
  }
  const T &GetDefault() const
  {
    return default_value_;
  }

  T default_value_ = T();
};
#if defined(__GNUC__)
#pragma GCC diagnostic pop
#endif

template <typename T> struct ValueStorage<T, false>
{
  virtual ~ValueStorage() = default;

  void StoreValue(const T &V, bool is_default = false)
  {
    V_ = V;
    if (is_default) {
      default_value_ = V;
    }
  }
  T &GetValue()
  {
    return V_;
  }
  T GetValue() const
  {
    return V_;
  }
  operator T() const
  {
    return GetValue();
  }
  const T &GetDefault() const
  {
    return default_value_;
  }
  T operator->() const
  {
    return V_;
  }

  T V_ = T();
  T default_value_ = T();
};
}

/// Option
namespace yaco {
struct OptBase : public cxxopts::Option
{
  template <class ... Args>
  explicit OptBase(Args && ... args)
      : cxxopts::Option(std::forward<Args>(args)...) {}
  ~OptBase() = default;

  virtual void Reset() = 0;

  virtual bool IsSpecified() const = 0;
  virtual bool IsPositionRelated() const = 0;
  virtual Occurrence getOccurrence() const = 0;

  virtual ErrOr<void> ApplyValue(const cxxopts::OptionValue &OV) = 0;
  virtual size_t hash() const  = 0;
  virtual std::shared_ptr<cxxopts::OptionDetails> details() const = 0;
};

template <class DataType,
          class VS = ValueStorage<DataType, std::is_class<DataType>::value>>
struct Opt final
    : public OptBase,
      public VS
{
  template <class ... Action>
  explicit Opt(const Action & ... actions)
      : OptBase("",
                "",
                cxxopts::value<DataType>(VS::GetValue()),
                "",
                std::make_shared<cxxopts::Alternatives>())
  {
    ApplyAction(*this, actions...);
    Done();
  }
  Opt(const Opt<DataType> &) = delete;
  Opt<DataType> &operator=(const Opt<DataType> &other) = delete;

  ~Opt() override
  {
    parser_ = nullptr;
  }

  void Done() ;

  bool IsSpecified() const override
  {
    return count_ > 0;
  }

  bool IsPositionRelated() const override
  {
    return formatting_flags_ == FormattingFlags::Positional;
  }

  Occurrence getOccurrence() const override {
    return occurrence_;
  }

  ErrOr<void> ApplyValue(const cxxopts::OptionValue &OV) override
  {
    count_ = OV.count();
    if (count_ > 0)
    {
      set_value_cb_(OV.template as<DataType>());
    }
    return true;
  }

  // ErrOr<void> StoreValue(const cxxopts::OptionValue &OV, bool is_default) override
  // {
  //   auto err_or_value = OV.template as_safe<DataType>();
  //   if (err_or_value.bad())
  //   {
  //     return false;
  //   }
  //   VS::StoreValue(err_or_value.get(), is_default);
  //   return true;
  // }

  size_t hash() const override
  {
    return option_details_->hash();
  }
  std::shared_ptr<cxxopts::OptionDetails> details() const override
  {
    return option_details_;
  }

  template <typename T>
  void StoreValue(const T &V, bool is_default = false)
  {
    VS::StoreValue(V, is_default);
  }

  void Reset() override
  {
    const_cast<cxxopts::Value *>(this->value_.get())->reset_value();
    this->StoreValue(this->GetDefault(), true);
  }

  struct Parser *parser_ = nullptr;
  std::shared_ptr<cxxopts::OptionDetails> option_details_ = nullptr;
  std::string category_ = "";
  FormattingFlags formatting_flags_ = FormattingFlags::NormalFormatting;
  Occurrence occurrence_ = Occurrence::Optional;
  ValueAllow value_allow_ = ValueAllow::ValueRequired;
  size_t count_ = 0;
  std::function<void(const DataType &)> set_value_cb_ = [](const DataType &) {};
};

// Parser
struct Parser : public cxxopts::Options
{
  template <typename ... Actions>
  explicit Parser(const Actions & ... actions)
      : cxxopts::Options("")
      , opt_list()
  {
    ApplyAction(*this, actions...);
  }

  ~Parser() override = default;

  Options &program_name(std::string name)
  {
    this->m_program = std::move(name);
    return *this;
  }

  const std::string &program_name() const
  {
    return this->m_program;
  }

  int enparse(int argc, const char* const* argv)
  {
    if (this->m_program.empty())
    {
      this->program_name(argv[0]);
    }
    auto result = this->parse(argc, argv);
    if (result.bad()) {
      return 1;
    }
    if (AssignValues(*this, result.get()).bad()) {
      return 2;
    }
    return 0;
  }

  void ResetAllOptions()
  {
    for (auto &item : opt_list)
    {
      item->Reset();
    }
  }

  std::vector<OptBase *> opt_list;

  // Assign the calculated values to each option variable.
  static ErrOr<void> AssignValues(const Parser &parser, const cxxopts::ParseResult &result)
  {
    for (OptBase *opt_base : parser.opt_list)
    {
      auto err_or_val_detail = result[opt_base->details()];
      if (err_or_val_detail.bad())
      {
        return false;
      }
      const cxxopts::OptionValue &OV = err_or_val_detail.get();
      opt_base->ApplyValue(OV);
    }
    return true;
  }

};

Parser &GlobalParser();

template <class DataType>
void OtherDone(Opt<DataType> &O)
{
  (void)O;
}

template <bool>
void OtherDone(Opt<bool> &O)
{
  O.value_allow_ = ValueAllow::ValueOptional;
}

template <class DataType, class VS>
void Opt<DataType, VS>::Done()
{
  if (!parser_ && !(parser_ = &GlobalParser()))
  {
    cxxopts::throw_or_mimic<option_nonbind_error>(opts_);
    return;
  }

  if (alternatives_ && !alternatives_->empty())
  {
    std::string alters_str;
    std::string alters_detail;
    for (size_t idx = 0, all = alternatives_->size(); idx < all; ++idx)
    {
      const cxxopts::Alternative &alter = alternatives_->operator[](idx);
      alters_str += alter.name + "|";
      alters_detail += "\n" + alter.name + " - " + alter.description;
    }
    if (!alters_str.empty()) {
      alters_str.pop_back();
    }

    if (arg_help_.empty())
    {
      arg_help_ = alters_str;
    }
    else
    {
      arg_help_ += ":" + alters_str;
    }
    desc_ += alters_detail;
  }

  option_details_ = nullptr;
  if (!opts_.empty())
  {
    parser_->add_option(category_, *this);
    auto err_or_qkeys = cxxopts::values::parser_tool::SplitSwitchDef(opts_);
    if (err_or_qkeys.bad())
    {
      return;
    }
    std::string query_key_;
    auto qkeys = err_or_qkeys.get();
    if (!std::get<0>(qkeys).empty())
    {
      query_key_ = std::get<0>(qkeys);
    }
    else
    {
      query_key_ = std::get<1>(qkeys);
    }
    option_details_ = parser_->get_option_detail(query_key_);
  }
  else
  {
    option_details_ = std::make_shared<cxxopts::OptionDetails>
    (
      "",
      arg_help_,
      desc_,
      value_,
      nullptr,
      parser_->option_count()
    );
  }

  if (IsPositionRelated())
  {
    parser_->parse_positional(option_details_, false);
  }

  OtherDone<DataType>(*this);

  parser_->opt_list.push_back(this);

}

}   // namespace yaco

/// Applicators - the instance properties implicit modifier.
namespace yaco
{

template <typename T>
struct val2str
{
  static std::string make(const T &val) { return std::to_string(val); }
};

template <unsigned N>  struct applicator <char[N]>
{
  static void apply(const char *str, Parser &Os)
  {
    Os.program_name(std::string(str));
  }

  template <class DataType>
  static void apply(const char *str, Opt<DataType> &O)
  {
    if (!O.opts_.empty())
    {
      cxxopts::throw_or_mimic<option_multiset_error>
          (std::string(str) + " (previous: " + O.opts_ + ")");
      return;
    }
    O.opts_ = std::string(str);
  }
};

template <> struct applicator<std::string>
{
  static void apply(const std::string &str, Parser &Os)
  {
    Os.program_name(str);
  }
};

template <> struct applicator<FormattingFlags>
{
  template <typename DataType>
  static void apply(FormattingFlags formattingFlags, Opt<DataType> &O)
  {
    O.formatting_flags_ = formattingFlags;
  }
};

template <> struct applicator<Occurrence>
{
  template <typename DataType>
  static void apply(Occurrence occurrence, Opt<DataType> &O)
  {
    O.occurrence_ = occurrence;
  }
};

template <> struct applicator<ValueAllow>
{
  template <typename DataType>
  static void apply(ValueAllow valueAllow, Opt<DataType> &O)
  {
    O.value_allow_ = valueAllow;
    if (valueAllow == ValueAllow::ValueOptional) {
      ((cxxopts::Value *)O.value_.get())->
          implicit_value(val2str<DataType>::make({}));
    }
  }
};

}   // namespace yaco

/// The instance properties explicit modifier.
namespace yaco {

struct desc
{
  explicit desc(const char *str) : desc_(std::string(str)) {}
  template <class DataType>
  void Apply(Opt<DataType> &O) const
  {
    O.desc_ = desc_;
  }
  std::string desc_;
};

struct inject
{
  explicit inject(Parser &Os) : Os_(Os) {}
  template <class DataType>
  void Apply(Opt<DataType> &O) const
  {
    if (O.parser_)
    {
      cxxopts::throw_or_mimic<option_multibind_error>(O.opts_);
      return;
    }
    O.parser_ = &Os_;
  }
  Parser &Os_;
};

// Config category.
struct cat
{
  explicit cat(const char *name) : category_name_(std::string(name)) {}
  template <typename DataType>
  void Apply(Opt<DataType> &O) const
  {
    if (!O.category_.empty())
    {
      cxxopts::throw_or_mimic<category_multibind_error>(O.opts_);
      return;
    }
    O.category_ = category_name_;
  }
  std::string category_name_;
};

template <>
struct val2str<std::string>
{
  static std::string make(const std::string &val) { return val; }
};

template<template<typename...> class C, typename U>
struct val2str <C<U>>
{
  static std::string make(std::vector<U> vec)
  {
    std::string buf;
    for (auto iter = vec.begin(), end = vec.end(); iter != end; ++iter)
    {
      buf.append(val2str<U>::make(*iter)).append(1, CXXOPTS_VECTOR_DELIMITER);
    }
    if (!buf.empty())
    {
      buf.pop_back();
    }
    return buf;
  }
};

template <unsigned N>
struct val2str <char[N]>
{
  static std::string make(const char *str) { return std::string(str); }
};

template <typename T>
struct initializer
{
  explicit initializer(const T &def_val) : def_value_(def_val) {}
  template <typename DataType>
  void Apply(Opt<DataType> &O) const
  {
    O.StoreValue(def_value_, true);
    ((cxxopts::Value *)O.value_.get())->
        default_value(val2str<T>::make(def_value_));
  }
  const T &def_value_;
};

template <typename T> struct initializer<T> init(const T &def_val)
{
  return initializer<T>(def_val);
}

struct value_desc
{
  explicit value_desc(const std::string &desc) : desc_(desc) {}
  template <typename DataType>
  void Apply(Opt<DataType> &O) const
  {
    O.arg_help_ = desc_;
  }

  std::string desc_;
};

namespace detail
{
template <typename F>
struct callback_traits : public callback_traits<decltype(&F::operator())>
{
};

template <typename R, typename C, typename... Args>
struct callback_traits<R (C::*)(Args...) const>
{
  using result_type = R;
  using arg_type = typename std::tuple_element<0, std::tuple<Args...>>::type;
  static_assert(sizeof...(Args) == 1,
                "Callback function must have one and only one parameter.");
  static_assert(std::is_same<result_type, void>::value,
                "Callback return type must be void.");
  static_assert(std::is_lvalue_reference<arg_type>::value,
                "Callback arg_type must be a lvalue reference.");
  static_assert(std::is_const<typename std::remove_reference<arg_type>::type>::value,
                "Callback arg_type must be a const.");
};
}

template <typename R, typename T> struct cb
{
  explicit cb(std::function<R(T)> fn) : callback_(fn) {}
  template <class DataType>
  void Apply(Opt<DataType> &O) const { O.set_value_cb_ = callback_; }

  std::function<R(T)> callback_;
};

template <typename F>
cb<typename detail::callback_traits<F>::result_type,
   typename detail::callback_traits<F>::arg_type>
callback(F fn)
{
  using result_type = typename detail::callback_traits<F>::result_type;
  using arg_type = typename detail::callback_traits<F>::arg_type;
  return cb<result_type, arg_type>(fn);
}

using OptionEnumValue = cxxopts::Alternative;

#define coEnumVal(ENUMVAL, DESC)              \
  yaco::OptionEnumValue(#ENUMVAL, int(ENUMVAL), DESC)
#define coEnumValN(ENUMVAL, FLAG_NAME, DESC)  \
  yaco::OptionEnumValue(FLAG_NAME, int(ENUMVAL), DESC)

class ValuesClass
{
private:
  std::vector<OptionEnumValue> values_;
public:
  explicit ValuesClass(std::initializer_list<OptionEnumValue> options)
      : values_(options) {}

  template <class DataType>
  void Apply(Opt<DataType> &O) const
  {
    if (O.alternatives_ == nullptr)
    {
      O.alternatives_ =
          std::make_shared<cxxopts::Alternatives>();
    }
    for (auto &value : values_)
    {
      O.alternatives_->template emplace_back(value);
    }
  }
};

template <typename ... Opts>
ValuesClass values(Opts && ... options)
{
  return ValuesClass({options...});
}

}   // namespace yaco

// Make global/implicit parser support.
namespace yaco
{

template <typename T> class Singleton
{
public:
  static T &get_instance()
  {
    static T *instance;
    if (instance == nullptr)
    {
      instance = new T{};
    }
    return *instance;
  }

protected:
  Singleton() = default;
  ~Singleton() = default;
};

template class Singleton<Parser>;

inline Parser &GlobalParser()
{
  return Singleton<Parser>::get_instance();
}

inline int enparse(int argc, const char* const* argv)
{
  return GlobalParser().enparse(argc, argv);
}

inline std::string help(const std::vector<std::string>& groups = {})
{
  return GlobalParser().help(groups);
}

}   // namespace yaco

#endif // CXXOPTS_YACO_H
